package jpm.main;

import static jpm.common.AssertUtils.ensure;
import static jpm.common.AssertUtils.ensureNotNull;

import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import jpm.common.JPMException;
import jpm.common.JPMException.ErrorCode;
import jpm.common.Utils;
import jpm.position.Cache;
import jpm.position.Cache.UpdateStatus;
import jpm.trade.Trade;
import jpm.trade.TradeHelper;
import jpm.trade.TradeHelper.Operation;

public class TradeSimulator {
	public static void playEventsFromFile(String file, String delimiter,
			boolean multipleThreads, boolean shuffle,
			final PrintStream printStream) throws Throwable {

		ensureFileExistsInCurrDir(file);
		
		List<String> strings = Files.readAllLines(Paths.get(file),
				Charset.defaultCharset());

		List<Trade> trades = getTradeProps(strings, delimiter);

		if (printStream != null)
			printStream.println("read [" + trades.size() + "] trades");

		if (shuffle) {
			Collections.shuffle(trades);
		}

		int numThreads = 1;
		if (multipleThreads)
			numThreads = 8;

		ExecutorService executorService = Executors
				.newFixedThreadPool(numThreads);

		List<Future<?>> futures = new ArrayList<>(trades.size());
		for (int i = 0; i < trades.size(); i++) {

			final Trade trade = trades.get(i);
			Future<?> future = executorService.submit(new Runnable() {

				@Override
				public void run() {

					StopWatch sw = StopWatch.createStarted();
					UpdateStatus updateStatus = Cache.update(trade);

					if (printStream != null)
						printStream.println("Thread ("
								+ Thread.currentThread().getId() + "): "
								+ asString(trade) + ": " + updateStatus + " ("
								+ sw.nsElapsed() + " ns)");
				}
			});

			futures.add(future);
		}

		// this is done to push any thread level exception to the main thread
		for (int i = 0; i < trades.size(); i++) {
			try {
				futures.get(i).get();
			} catch (ExecutionException e) {
				throw e.getCause();
			}
		}

		executorService.shutdown();
		executorService.awaitTermination(30, TimeUnit.SECONDS);
	}

	private static void ensureFileExistsInCurrDir(String file) {
		
		if (!Files.exists(Paths.get(file), LinkOption.NOFOLLOW_LINKS))
			throw new JPMException().errorCode(ErrorCode.ERROR_INVALID_INPUT)
					.message(
							"File [" + file
									+ "] missing in current directory ["
									+ Paths.get("").toAbsolutePath() + "]");
	}

	private static String asString(Trade trade) {
		TradeHelper.ensureValid(trade);

		return trade.getTradeId() + "|" + trade.getTradeVersion() + "|"
				+ trade.getSecurity() + "|" + trade.getQuantity() + "|"
				+ (trade.isBuy() ? "BUY" : "SELL") + "|" + trade.getAccountNo()
				+ "|" + trade.getOperation();
	}

	private static List<Trade> getTradeProps(List<String> strings,
			String delimiter) {

		ensureNotNull(strings, "strings is null");
		ensure(!strings.isEmpty(), "No trade events");
		ensureNotNull(delimiter, "regex is null");

		List<Trade> tradeProps = new ArrayList<>();

		int i = 0;
		for (String string : strings) {
			if (string.isEmpty())
				continue;

			if (string.startsWith("#"))
				continue; // comment line

			final String FORMAT_DESC = ". The format is: Trade ID|Version|Security Identifier|Quantity|Direction|Account|Operation. New lines will be ignored and comment line should start with #";

			String split[] = string.split(Pattern.quote(delimiter));
			if (split.length != 7) {
				throw new JPMException()
						.errorCode(ErrorCode.ERROR_INVALID_INPUT)
						.message(
								"Error in row ["
										+ (i + 1)
										+ "]: Invalid number of tokens after splitting by '"
										+ delimiter
										+ "'. Expected [7] but found ["
										+ split.length + "]" + FORMAT_DESC);
			}

			int tradeId;
			if (!Utils.canParseAsInt(split[0]))
				throw new JPMException().errorCode(
						ErrorCode.ERROR_INVALID_INPUT).message(
						"Error in row [" + (i + 1) + "]: Specified trade id ["
								+ split[0] + " in first column is not numeric"
								+ FORMAT_DESC);
			tradeId = Utils.safeParseAsInt(split[0]);

			int version;
			if (!Utils.canParseAsInt(split[1]))
				throw new JPMException().errorCode(
						ErrorCode.ERROR_INVALID_INPUT).message(
						"Error in row [" + (i + 1) + "]: Specified version ["
								+ split[1] + " in second column is not numeric"
								+ FORMAT_DESC);
			version = Utils.safeParseAsInt(split[1]);

			String security = split[2];

			int quantity;
			if (!Utils.canParseAsInt(split[3]))
				throw new JPMException().errorCode(
						ErrorCode.ERROR_INVALID_INPUT).message(
						"Error in row [" + (i + 1) + "]: Specified quantity ["
								+ split[3] + " in fourth column is not numeric"
								+ FORMAT_DESC);
			quantity = Utils.safeParseAsInt(split[3]);
			if (quantity < 0)
				throw new JPMException().errorCode(
						ErrorCode.ERROR_INVALID_INPUT).message(
						"Error in row [" + (i + 1) + "]: Specified quanitty ["
								+ split[3]
								+ " in fourth column is not positive");

			boolean buy;
			split[4] = split[4].toUpperCase();
			if (split[4].equals("BUY"))
				buy = true;
			else if (split[4].equals("SELL"))
				buy = false;
			else
				throw new JPMException()
						.errorCode(ErrorCode.ERROR_INVALID_INPUT)
						.message(
								"Error in row ["
										+ (i + 1)
										+ "]: Specified side ["
										+ split[4]
										+ " in fifth column is invalid. Expected either 'BUY' or 'SELL' (case insensitive without quotes)");

			String accountNo = split[5];

			Operation operation;
			split[6] = split[6].toUpperCase();
			if (split[6].equals("NEW"))
				operation = Operation.NEW;
			else if (split[6].equals("AMEND"))
				operation = Operation.AMEND;
			else if (split[6].equals("CANCEL"))
				operation = Operation.CANCEL;
			else
				throw new JPMException()
						.errorCode(ErrorCode.ERROR_INVALID_INPUT)
						.message(
								"Error in row ["
										+ (i + 1)
										+ "]: Specified operation ["
										+ split[6]
										+ " in seventh column is invalid. Expected either 'NEW' or 'AMEND' or 'CANCEL' (case insensitive without quotes)");

			Trade tradeProp = new Trade(tradeId, version, security, quantity,
					buy, accountNo, operation);
			tradeProps.add(tradeProp);
		}

		return tradeProps;
	}
}
